/**
 * 
 */
package com.cipe.cmrs.dao;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.commons.beanutils.PropertyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;

import com.cipe.cmrs.exception.QueryException;
import com.cipe.cmrs.model.BaseEntity;

/**
 * This class is the base dao for the application. It provides the generic
 * methods for interacting with database for CRUD operations.
 * 
 * @author Incepio
 */

@Repository
public class BaseDao<T extends BaseEntity> {

	protected Logger logger = LoggerFactory.getLogger(getClass());

	private static final String NULL_ID_MESSAGE = "Id cannot be null.";

	@PersistenceContext
	protected EntityManager em;

	/**
	 * Find the entity of T with a given id.
	 * 
	 * @param id
	 * @return
	 */
	public T findById(Class<T> persistanceClass, Serializable id) {
		if (id == null) {
			throw new IllegalArgumentException(NULL_ID_MESSAGE);
		}
		return em.find(persistanceClass, id);
	}

	/**
	 * Delete the given entity.
	 * 
	 * @param entity
	 */
	public void delete(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Cannot delete null entity.");
		}
		em.remove(entity);
	}

	/**
	 * Delete the entity of type T with the given Id.
	 * 
	 * @param id
	 */
	public void delete(Class<T> persistanceClass, Serializable id) {
		if (id == null) {
			throw new IllegalArgumentException(NULL_ID_MESSAGE);
		}
		em.remove(em.find(persistanceClass, id));
	}

	/**
	 * Returns all entities of type T.
	 * 
	 * @return
	 */
	public List<T> findAll(String entityName) {
		String sql = "select b from " + entityName + " b";
		return getResultList(sql);
	}

	/**
	 * Add a new entity.
	 * 
	 * @param entity
	 * @return
	 */
	public Serializable persist(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Cannot persist null entity.");
		}
		em.persist(entity);
		return entity.getId();
	}

	/**
	 * 
	 * @param namedQuery
	 * @param parameters
	 * @return
	 */

	public int executeUpdateNamedQuery(String namedQuery, Object[] parameters) {
		try {
			return executeUpdateNamedQuery(namedQuery, parameters, null);
		} catch (NoResultException e) {
			return 0;
		}
	}

	/**
	 * 
	 * @param namedQuery
	 * @param parameters
	 * @param parametersName
	 * @return
	 */

	public int executeUpdateNamedQuery(String namedQuery, Object[] parameters,
			Object[] parametersName) {
		try {
			return getPreparedNamedQuery(namedQuery, parameters, parametersName)
					.executeUpdate();
		} catch (NoResultException e) {
			return 0;
		}
	}

	/**
	 * Result list for a JPQL query.
	 * 
	 * @param query
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getResultList(String query) {
		try {
			return em.createQuery(query).getResultList();
		} catch (NoResultException e) {
			return new ArrayList<T>();
		}
	}

	/*
	 * @SuppressWarnings("unchecked") public List<T> getResultList(String
	 * query,Class<T> class1) { try{ return
	 * em.createQuery(query,class1).getResultList(); } catch(NoResultException
	 * e){ return new ArrayList<T>(); } }
	 */

	@SuppressWarnings("unchecked")
	public List<T> getResultList(String query, Object[] parameters) {
		try {
			Query q = em.createQuery(query);
			for (int i = 0; i < parameters.length; i++) {
				q.setParameter(i + 1, parameters[i]);
			}
			return q.getResultList();
		} catch (NoResultException e) {
			return new ArrayList<T>();
		}
	}

	/**
	 * A single result, like count, max or a single row/column combo etc., for a
	 * native SQL query.
	 * 
	 * @param query
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public T getSingleResult(String sqlQuery) {
		try {
			return (T) em.createQuery(sqlQuery).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	public T getSingleResult(String query, Object[] parameters) {
		T t = null;
		Query q = em.createQuery(query);
		for (int i = 0; i < parameters.length; i++) {
			q.setParameter(i + 1, parameters[i]);
		}
		try {
			t = (T) q.getSingleResult();
		} catch (NoResultException e) {
			logger.error("No result found for query " + query);
		}
		return t;
	}

	/***************************************************************************************************
	 * 
	 * @param namedQuery
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public T getSingleNamedQueryResult(String namedQuery) {
		try {
			return (T) em.createNamedQuery(namedQuery).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	public Object getSingleNamedQueryScalerResult(String namedQuery) {
		try {
			return em.createNamedQuery(namedQuery).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	/****************************************************************************************************
	 * 
	 * @param namedQuery
	 * @param parameters
	 * @return
	 */
	public T getSingleNamedQueryResult(String namedQuery, Object[] parameters) {
		try {
			return getSingleNamedQueryResult(namedQuery, parameters, null);
		} catch (NoResultException e) {
			return null;
		}
	}

	public Object getSingleNamedQueryScalerResult(String namedQuery,
			Object[] parameters) {
		try {
			return getSingleNamedQueryScalerResult(namedQuery, parameters, null);
		} catch (NoResultException e) {
			return null;
		}
	}

	/*****************************************************************************************************
	 * 
	 * @param namedQuery
	 * @param parameters
	 * @return
	 */
	public List<T> getNamedQueryResultList(String namedQuery,
			Object[] parameters) {
		try {
			return getNamedQueryResultList(namedQuery, parameters, null);
		} catch (NoResultException exception) {
			return new ArrayList<T>();
		}
	}

	public List<? extends Object> getNamedQueryScalerResultList(
			String namedQuery, Object[] parameters) {
		try {
			return getNamedQueryScalerResultList(namedQuery, parameters, null);
		} catch (NoResultException exception) {
			return null;
		}
	}

	/****************************************************************************************************
	 * 
	 * @param namedQuery
	 * @param parameters
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public T getSingleNamedQueryResult(String namedQuery, Object[] parameters,
			Object[] parametersName) {
		try {
			return (T) getPreparedNamedQuery(namedQuery, parameters,
					parametersName).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	public Object getSingleNamedQueryScalerResult(String namedQuery,
			Object[] parameters, Object[] parametersName) {
		try {
			return getPreparedNamedQuery(namedQuery, parameters, parametersName)
					.getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	/*****************************************************************************************************
	 * 
	 * @param namedQuery
	 * @param parameters
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getNamedQueryResultList(String namedQuery,
			Object[] parameters, Object[] parametersName) {
		try {
			return getPreparedNamedQuery(namedQuery, parameters, parametersName)
					.getResultList();
		} catch (NoResultException exception) {
			return new ArrayList<T>();
		}
	}

	@SuppressWarnings("unchecked")
	public List<? extends Object> getNamedQueryScalerResultList(
			String namedQuery, Object[] parameters, Object[] parametersName) {
		try {
			return getPreparedNamedQuery(namedQuery, parameters, parametersName)
					.getResultList();
		} catch (NoResultException exception) {
			return null;
		}
	}

	/*****************************************************************************************************
	 * 
	 * @param namedQueryName
	 * @param parameters
	 * @return
	 */
	private Query getPreparedNamedQuery(String namedQueryName,
			Object[] parameters, Object[] parametersName) {

		if (parametersName != null && parameters != null
				&& parametersName.length != parameters.length) {
			throw new RuntimeException(
					"length of parameters and parameters name array are not equals. parameters = "
							+ parameters.length + " -- parametersName = "
							+ parametersName.length);
		}

		Query namedQuery = em.createNamedQuery(namedQueryName);

		if (parameters != null && parameters.length > 0) {
			for (int i = 1; i <= parameters.length; i++) {
				if (parametersName == null) {
					namedQuery.setParameter(i, parameters[i - 1]);
				} else {
					namedQuery.setParameter(
							String.valueOf(parametersName[i - 1]),
							parameters[i - 1]);
				}
			}
		}

		return namedQuery;
	}

	/**
	 * Counts all the rows in the table for entity type T.
	 * 
	 * @return count of all the entities of type T
	 */
	public Integer countAll(String entityName) {
		String sql = "select count(o) from " + entityName + " o";
		return (Integer) em.createQuery(sql).getSingleResult();
	}

	/**
	 * Refresh the state of the instance from the database, overwriting changes
	 * made to the entity, if any.
	 * 
	 * @param entity
	 */
	public void refresh(T entity) {
		em.refresh(entity);
	}

	/**
	 * Merge the state of the given entity into the current persistence context. <br>
	 * <br>
	 * Simply delegates to merge(T entity) method in this class.
	 * 
	 * @see #merge
	 * @param entity
	 * @return the updated entity instance
	 */
	public T update(T entity) {
		return this.merge(entity);
	}

	/**
	 * Merge the state of the given entity into the current persistence context.
	 * 
	 * @see #update
	 * @param entity
	 * @return The updated entity instance. The state of the passed entity is
	 *         not changed; that is, it doesn't become managed.
	 */
	public T merge(T entity) {
		return em.merge(entity);
	}

	/**
	 * Detach the state of the given entity from the current persistence
	 * context.
	 * 
	 * @see #update
	 * @param entity
	 * @return The detached entity instance.
	 */
	public void detach(T entity) {
		em.detach(entity);
	}

	public void flush() {
		em.flush();
	}

	/**
	 * This will get List of Object by throwing the native query. Which is totally depend on the database
	 * @author Krishna
	 * @param query
	 * @param clazz
	 * @param id
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getResultListWithNativeQuery(String query, Class<T> clazz,
			Integer id) {
		try {

			return em.createNativeQuery(
					"select * from " + query + "(" + id + ")", clazz)
					.getResultList();
		} catch (NoResultException e) {
			System.out.println(e);
			e.printStackTrace();
			return new ArrayList<T>();
		} catch (Exception e) {
			e.printStackTrace();
			return new ArrayList<T>();
		}
	}
	/**
	 * This will get List of Object by throwing the native query. Which is totally depend on the database
	 * @author Krishna
	 * @param query
	 * @param clazz
	 * @param id
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getResultListWithNativeQuery(String query, Class<T> clazz,
			Integer uid, Integer regid) {
		try {
			return em.createNativeQuery(
					"select * from " + query + "(" + uid + "," + regid + ")",
					clazz).getResultList();
		} catch (NoResultException e) {
			System.out.println(e);
			e.printStackTrace();
			return new ArrayList<T>();
		} catch (Exception e) {
			e.printStackTrace();
			return new ArrayList<T>();
		}
	}
	/**
	 * This will get List of Object by throwing the native query. Which is totally depend on the database
	 * @author Krishna
	 * @param query
	 * @param clazz
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getResultListWithNativeQuery(String query, Class<T> clazz) {
		try {
			return em.createNativeQuery("select * from " + query, clazz)
					.getResultList();
		} catch (NoResultException e) {
			System.out.println(e);
			e.printStackTrace();
			return new ArrayList<T>();
		} catch (Exception e) {
			e.printStackTrace();
			return new ArrayList<T>();
		}
	}

	/**
	 * 
	 * @param queryString
	 * @param parameters
	 * @return
	 */
	public List<T> getResultList(String queryString, Class<T> clazz,
			final String[] columnNames, Object[] parameters)
			throws QueryException {
		Assert.notEmpty(columnNames);
		try {
			Query q = em.createQuery(queryString);
			if (parameters != null) {
				for (int i = 0; i < parameters.length; i++) {
					q.setParameter(i + 1, parameters[i]);
				}
			}
			return convert(q.getResultList(), clazz, columnNames);
		} catch (Exception e) {
			throw new QueryException(e);
		}
	}

	public List<T> getResultParameter(String queryString, Class<T> clazz,
			final String[] param, Object[] parameters) throws QueryException {
		Assert.notEmpty(param);
		try {
			Query q = em.createQuery(queryString);
			if (parameters != null) {
				for (int i = 0; i < parameters.length; i++) {
					q.setParameter(param[i], parameters[i]);
				}
			}
			return (List<T>) q.getResultList();
			// return convert(q.getResultList(), clazz, columnNames);
		} catch (Exception e) {
			throw new QueryException(e);
		}
	}

	// TODO: Need to put this to sqlutil
	public BigDecimal getSum(Class<T> clazz, String columnToSum,
			List<String> conditions, List<Object> parameters) {
		StringBuilder queryBuilder = new StringBuilder("SELECT SUM(obj."
				+ columnToSum + ") FROM " + clazz.getName() + " obj ");
		if (conditions != null) {
			queryBuilder.append(" WHERE ");
			int i = 0;
			for (; i < conditions.size() - 1; i++) {
				queryBuilder.append("obj." + conditions.get(i));
				queryBuilder.append(" AND ");
			}
			queryBuilder.append("obj." + conditions.get(i));
		}

		Query q = em.createQuery(queryBuilder.toString());
		if (parameters != null) {
			for (int i = 1; i <= parameters.size(); i++) {
				q.setParameter(i, parameters.get(i - 1));
			}
		}

		Object result = q.getSingleResult();
		if (result != null) {
			return new BigDecimal(result.toString());
		} else {
			return BigDecimal.ZERO;
		}
	}

	@SuppressWarnings("rawtypes")
	private List<T> convert(List queryResultObject, Class<T> clazz,
			final String[] columnNames) throws NoSuchMethodException,
			InvocationTargetException, IllegalAccessException,
			InstantiationException {
		List<T> results = new ArrayList<T>();
		Iterator iter = queryResultObject.iterator();
		while (iter.hasNext()) {
			T bean = clazz.newInstance();
			if (columnNames.length > 1) {
				Object[] row = (Object[]) iter.next();
				for (int j = 0; j < row.length; j++) {
					if (row[j] != null) {
						initialisePath(bean, columnNames[j]);
						PropertyUtils.setProperty(bean, columnNames[j], row[j]);
					}
				}
			} else {
				initialisePath(bean, columnNames[0]);
				PropertyUtils.setProperty(bean, columnNames[0],
						(Object) iter.next());
			}
			results.add(bean);
		}
		return results;
	}

	@SuppressWarnings("rawtypes")
	private void initialisePath(final Object bean, final String fieldName)
			throws NoSuchMethodException, InvocationTargetException,
			IllegalAccessException, InstantiationException {
		int dot = fieldName.indexOf('.');
		while (dot >= 0) {
			String attributeName = fieldName.substring(0, dot);
			Class attributeClass = PropertyUtils.getPropertyType(bean,
					attributeName);
			if (PropertyUtils.getProperty(bean, attributeName) == null) {
				PropertyUtils.setProperty(bean, attributeName,
						attributeClass.newInstance());
			}
			dot = fieldName.indexOf('.', dot + 1);
		}
	}

	public Query createQuery(String sqlString) {
	    return em.createQuery(sqlString);
	}
}